﻿/* 
 * Copyright (c) Intel Corporation
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * -- Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * -- Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * -- Neither the name of the Intel Corporation nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE INTEL OR ITS
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Text;
using System.Web;
using System.Xml;
using DotNetOpenAuth;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.Messaging.Bindings;
using DotNetOpenAuth.OAuth;
using DotNetOpenAuth.OAuth.ChannelElements;
using DotNetOpenAuth.OAuth.Messages;
using DotNetOpenAuth.OpenId;
using DotNetOpenAuth.OpenId.ChannelElements;
using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;
using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
using DotNetOpenAuth.OpenId.RelyingParty;
using ExtensionLoader;
using ExtensionLoader.Config;
using HttpServer;
using Nwc.XmlRpc;
using OpenMetaverse;
using OpenMetaverse.Http;
using OpenMetaverse.StructuredData;
using CableBeachMessages;
using OAuthConsumer = DotNetOpenAuth.OAuth.WebConsumer;

namespace WorldServer.Extensions
{
    public class OpenIDLogin : IExtension<WorldServer>
    {
        const int INTERNAL_ERROR = -32603;
        const int REQUEST_TIMEOUT = 1000 * 100;
        protected string LOGIN_PAGE = WorldServer.WEB_CONTENT_DIR + "login.tpl";
        protected string LOGIN_SUCCESS_PAGE = WorldServer.WEB_CONTENT_DIR + "loginsuccess.tpl";

        protected WorldServer server;
        XmlRpcRequestDeserializer deserializer = new XmlRpcRequestDeserializer();
        InMemoryTokenManager tokenManager;
        OpenIdRelyingParty relyingParty = new OpenIdRelyingParty(new StandardRelyingPartyApplicationStore());
        Dictionary<string, Avatar> currentServiceRequests = new Dictionary<string, Avatar>();
        Dictionary<UUID, Avatar> currentSessions = new Dictionary<UUID, Avatar>();
        string defaultWelcomeMessage;
        int minimumLoginLevel = 0;
        string[] whitelist, blacklist;

        public OpenIDLogin()
        {
        }

        public bool Start(WorldServer server)
        {
            this.server = server;

            #region Config Loading

            try
            {
                IConfig opensimConfig = server.ConfigFile.Configs["OpenSimLogin"];
                IConfig openidConfig = server.ConfigFile.Configs["OpenID"];

                defaultWelcomeMessage = opensimConfig.GetString("DefaultWelcomeMessage");

                if (openidConfig.Contains("Whitelist"))
                    whitelist = openidConfig.GetString("Whitelist").Trim().Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                else if (openidConfig.Contains("Blacklist"))
                    blacklist = openidConfig.GetString("Blacklist").Trim().Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
            }
            catch (Exception)
            {
                Logger.Error("[OpenIDLogin] Failed to load [OpenSimLogin] or [OpenID] section from " + WorldServer.CONFIG_FILE);
                return false;
            }

            #endregion Config Loading

            // Setup the OAuth token manager with this server's DNS name as the consumer_key
            tokenManager = new InMemoryTokenManager(server.HttpUri.DnsSafeHost, null);

            server.HttpServer.AddHandler("GET", null, @"^/login/?$", WebGetHandler);
            server.HttpServer.AddHandler("POST", "application/x-www-form-urlencoded", @"^/login/?$", WebPostHandler);
            server.HttpServer.AddHandler("GET", null, @"^/login/openid_callback", OpenIDCallbackHandler);
            server.HttpServer.AddHandler("GET", null, @"^/login/oauth_callback", OAuthCallbackHandler);
            server.HttpServer.AddHandler("POST", "text/xml", @"^/login/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", LindenLoginMethod);
            server.HttpServer.AddHandler("POST", "application/x-www-form-urlencoded", @"^/login/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", LindenLoginMethod);

            return true;
        }

        public void Stop()
        {
        }

        #region OpenID/OAuth Handlers

        void WebGetHandler(IHttpClientContext client, IHttpRequest request, IHttpResponse response)
        {
            // Make sure the site is being visited through the hostname that will be used in our OpenID realm.
            // Otherwise, the OpenID login will fail
            if (request.Uri.Authority != server.HttpUri.Authority)
            {
                Uri redirect = new Uri(server.HttpUri, "/login/");

                Logger.Info("[OpenIDLogin] Redirecting from " + request.Uri + " to " + redirect);
                response.Status = HttpStatusCode.Found;
                response.AddHeader("Location", redirect.ToString());
                return;
            }

            // Authenticated sessionID is stored as a browser cookie
            RequestCookie sessionCookie = request.Cookies != null ? request.Cookies["sessionID"] : null;
            UUID sessionID = UUID.Zero;
            if (sessionCookie != null)
                UUID.TryParse(sessionCookie.Value, out sessionID);

            // Check if the client cookie matches an authenticated session
            Avatar avatar;
            if (sessionID != UUID.Zero && server.SessionProvider.TryGetAvatar(sessionID, out avatar))
            {
                // Client has an authenticated session, return a success response
                SendLoginSuccessTemplate(response, avatar.Identity, avatar.Attributes, sessionID);
            }
            else
            {
                // Client came here to login. Send the login form
                SendLoginTemplate(response, null, null);
            }
        }

        void WebPostHandler(IHttpClientContext client, IHttpRequest request, IHttpResponse response)
        {
            byte[] requestData = request.GetBody();
            string queryString = HttpUtility.UrlDecode(requestData, Encoding.UTF8);
            NameValueCollection query = System.Web.HttpUtility.ParseQueryString(queryString);
            string[] openidIdentifiers = query.GetValues("openid_identifier");

            Uri identity;
            Identifier identifier;

            if (openidIdentifiers != null && openidIdentifiers.Length == 1 &&
                UriIdentifier.TryParse(openidIdentifiers[0], out identifier) &&
                Uri.TryCreate(openidIdentifiers[0], UriKind.Absolute, out identity))
            {
                // Check if this identity is authorized for access
                if (IsIdentityAuthorized(identity))
                {
                    string baseURL = String.Format("{0}://{1}", server.HttpUri.Scheme, server.HttpUri.Authority);
                    Realm realm = new Realm(baseURL);

                    try
                    {
                        IAuthenticationRequest authRequest = relyingParty.CreateRequest(identifier, realm, new Uri(server.HttpUri, "/login/openid_callback"));

                        // Add a Simple Registration request to the OpenID request
                        ClaimsRequest sreg = new ClaimsRequest();
                        sreg.BirthDate = DemandLevel.Request;
                        sreg.Email = DemandLevel.Request;
                        sreg.FullName = DemandLevel.Request;
                        sreg.Gender = DemandLevel.Request;
                        sreg.Language = DemandLevel.Request;
                        sreg.Nickname = DemandLevel.Request;
                        sreg.TimeZone = DemandLevel.Request;
                        authRequest.AddExtension(sreg);

                        // Add an Attribute Exchange request to the OpenID request
                        FetchRequest ax = new FetchRequest();
                        ax.Attributes.AddOptional(AvatarAttributes.BIOGRAPHY.ToString());
                        ax.Attributes.AddOptional(AvatarAttributes.BIRTH_DATE.ToString());
                        ax.Attributes.AddOptional(AvatarAttributes.COMPANY.ToString());
                        ax.Attributes.AddOptional(AvatarAttributes.EMAIL.ToString());
                        ax.Attributes.AddOptional(AvatarAttributes.FIRST_NAME.ToString());
                        ax.Attributes.AddOptional(AvatarAttributes.LANGUAGE.ToString());
                        ax.Attributes.AddOptional(AvatarAttributes.LAST_NAME.ToString());
                        ax.Attributes.AddOptional(AvatarAttributes.TIMEZONE.ToString());
                        ax.Attributes.AddOptional(AvatarAttributes.WEBSITE.ToString());
                        authRequest.AddExtension(ax);

                        OpenAuthHelper.OpenAuthResponseToHttp(response, authRequest.RedirectingResponse);
                    }
                    catch (Exception ex)
                    {
                        SendLoginTemplate(response, null, "OpenID login failed: " + ex.Message);
                    }
                }
                else
                {
                    SendLoginTemplate(response, null, identity + " is not authorized to access this world");
                }
            }
            else
            {
                SendLoginTemplate(response, "Please fill in the OpenID URL field", null);
            }
        }

        void OpenIDCallbackHandler(IHttpClientContext client, IHttpRequest request, IHttpResponse response)
        {
            Uri identity;

            if (!String.IsNullOrEmpty(request.Uri.Query))
            {
                // OpenID handling
                IAuthenticationResponse authResponse = relyingParty.GetResponse(OpenAuthHelper.GetRequestInfo(request));

                if (authResponse != null)
                {
                    if (authResponse.Status == AuthenticationStatus.Authenticated)
                    {
                        // OpenID authentication succeeded
                        identity = new Uri(authResponse.ClaimedIdentifier.ToString());

                        // Check if this identity is authorized for access
                        if (IsIdentityAuthorized(identity))
                        {
                            #region Attributes

                            // Get the current attributes (if any) for this avatar from the AttributeProvider
                            Dictionary<Uri, OSD> attributes = server.AttributeProvider.GetAttributes(identity);

                            // Update the attributes with values retrieved from Simple Registration
                            SimpleRegistrationToAttributes(authResponse.GetExtension<ClaimsResponse>(), identity, ref attributes);

                            // Update the attributes with values retrieved from Attribute Exchange
                            AttributeExchangeToAttributes(authResponse.GetExtension<FetchResponse>(), ref attributes);

                            if (!attributes.ContainsKey(AvatarAttributes.FIRST_NAME) && !attributes.ContainsKey(AvatarAttributes.LAST_NAME))
                            {
                                attributes[AvatarAttributes.FIRST_NAME] = OSD.FromString(identity.ToString());
                                attributes[AvatarAttributes.LAST_NAME] = OSD.FromString("OpenID");
                            }

                            // Synchronize the attributes back to the AttributeProvider
                            server.AttributeProvider.UpdateAttributes(identity, attributes);

                            #endregion Attributes

                            #region Discovery

                            ServiceCollection services = GetServices(identity);

                            #endregion Discovery

                            #region Capability Retrieval

                            // Create an avatar object
                            Avatar avatar = new Avatar(identity, CableBeachUtils.IdentityToUUID(identity), attributes, services);

                            // Start the OAuth loop or return success
                            GetCapabilitiesOrCompleteLogin(avatar, null, response);

                            #endregion Capability Retrieval
                        }
                        else
                        {
                            SendLoginTemplate(response, null, identity + " is not authorized to access this world");
                        }

                        return;
                    }
                    else
                    {
                        string errorMsg;
                        if (authResponse.Exception != null)
                            errorMsg = authResponse.Exception.Message;
                        else
                            errorMsg = authResponse.Status.ToString();

                        SendLoginTemplate(response, null, errorMsg);
                        return;
                    }
                }

                //response.Status = HttpStatusCode.Redirect;
                //response.AddHeader("Location", RemoveQuery(request.Uri).ToString());
                response.Status = HttpStatusCode.BadRequest;
                response.AddToBody("Invalid or missing OpenID callback data");
            }
        }

        protected virtual ServiceCollection GetServices(Uri identity)
        {
            // TODO: Create an empty list of services for now, since we aren't using XRD(S) yet
            ServiceCollection services = new ServiceCollection();

            // XRD 
            // Overriding CableBeachServices.FILESYSTEM_WEBDAV service if OpenID can offer one
            Service webdavservice = SimpleServices.CreateServiceFromLRDD(new Uri(identity.ToString() + ";xrd"), new Uri(CableBeachServices.FILESYSTEM_WEBDAV), true, true);
            if (webdavservice != null)
                services.Add(new Uri(CableBeachServices.FILESYSTEM_WEBDAV), webdavservice);

            // Get a list of services to attach to this avatar. This will also contact any trusted
            // services to attempt capability fetching
            server.ServiceProvider.GetServices(identity, true, ref services);

            return services;
        }

        void SimpleRegistrationToAttributes(ClaimsResponse sreg, Uri claimedIdentifier, ref Dictionary<Uri, OSD> attributes)
        {
            if (sreg == null)
                return;

            #region Name Handling

            // Prefer FullName
            if (!String.IsNullOrEmpty(sreg.FullName))
            {
                Logger.Debug("[OpenIDLogin] SREG: Received FullName: " + sreg.FullName);
                int splitter = sreg.FullName.IndexOf(' ');
                if (splitter > -1)
                {
                    attributes[AvatarAttributes.FIRST_NAME] = OSD.FromString(sreg.FullName.Substring(0, splitter));
                    attributes[AvatarAttributes.LAST_NAME] = OSD.FromString(sreg.FullName.Substring(splitter + 1));
                }
                else if (String.IsNullOrEmpty(sreg.Nickname))
                {
                    sreg.Nickname = sreg.FullName;
                }
            }
            if (!String.IsNullOrEmpty(sreg.Email))
            {
                Logger.Debug("[OpenIDLogin] SREG: Received Email: " + sreg.Email);
                attributes[AvatarAttributes.EMAIL] = OSD.FromString(sreg.Email);

                // Fallback: Split Email into first and last name
                if (!attributes.ContainsKey(AvatarAttributes.FIRST_NAME))
                {
                    int splitter = sreg.Email.IndexOf('@');
                    if (splitter > -1)
                    {
                        attributes[AvatarAttributes.FIRST_NAME] = OSD.FromString(sreg.Email.Substring(0, splitter));
                        attributes[AvatarAttributes.LAST_NAME] = OSD.FromString(sreg.Email.Substring(splitter + 1));
                    }
                }
            }
            // Last ditch effort: Use Nickname (or FullName if it did not contain a space) as the first name and
            // the domain name of the claimed identity as the last name
            if (!attributes.ContainsKey(AvatarAttributes.FIRST_NAME) && !String.IsNullOrEmpty(sreg.Nickname))
            {
                attributes[AvatarAttributes.FIRST_NAME] = OSD.FromString(sreg.Nickname);
                attributes[AvatarAttributes.LAST_NAME] = OSD.FromString(claimedIdentifier.Authority);
            }

            #endregion Name Handling
            
            if (sreg.BirthDate.HasValue)
            {
                Logger.Debug("[OpenIDLogin] SREG: Received BirthDate: " + sreg.BirthDate.Value);
                attributes[AvatarAttributes.BIRTH_DATE] = OSD.FromDate(sreg.BirthDate.Value);
            }
            if (sreg.Gender.HasValue)
            {
                Logger.Debug("[OpenIDLogin] SREG: Received Gender: " + sreg.Gender.Value);
                // TODO:
            }
            if (!String.IsNullOrEmpty(sreg.Language))
            {
                Logger.Debug("[OpenIDLogin] SREG: Received Language: " + sreg.Language);
                attributes[AvatarAttributes.LANGUAGE] = OSD.FromString(sreg.Language);
            }
            if (!String.IsNullOrEmpty(sreg.TimeZone))
            {
                Logger.Debug("[OpenIDLogin] SREG: Received TimeZone: " + sreg.TimeZone);
                attributes[AvatarAttributes.TIMEZONE] = OSD.FromString(sreg.TimeZone);
            }
        }

        void AttributeExchangeToAttributes(FetchResponse ax, ref Dictionary<Uri, OSD> attributes)
        {
            if (ax == null)
                return;

            foreach (AttributeValues attribute in ax.Attributes)
            {
                if (attribute.Values.Count > 0)
                {
                    string firstValue = attribute.Values[0];
                    Logger.Debug("[OpenIDLogin] AX: Received " + attribute.TypeUri + ": " + firstValue);
                    attributes[new Uri(attribute.TypeUri)] = OSD.FromString(firstValue);
                }
            }
        }

        void OAuthCallbackHandler(IHttpClientContext client, IHttpRequest request, IHttpResponse response)
        {
            Avatar avatar;
            string requestToken = OpenAuthHelper.GetQueryValue(request.Uri.Query, "oauth_token");

            if (!String.IsNullOrEmpty(requestToken) && currentServiceRequests.TryGetValue(requestToken, out avatar))
            {
                Uri[] unassociatedCaps;
                Service service = GetCurrentService(avatar.Services, out unassociatedCaps);

                if (service != null)
                {
                    try
                    {
                        OAuthConsumer consumer = new OAuthConsumer(OpenAuthHelper.CreateServiceProviderDescription(service), tokenManager);
                        AuthorizedTokenResponse tokenResponse = consumer.ProcessUserAuthorization(OpenAuthHelper.GetRequestInfo(request));

                        // We actually don't need the access token at all since the capabilities should be in this response.
                        // Parse the capabilities out of ExtraData
                        Dictionary<Uri, Uri> newCaps = new Dictionary<Uri, Uri>();
                        foreach (KeyValuePair<string, string> capability in tokenResponse.ExtraData)
                        {
                            Uri capIdentifier, capUri;
                            if (Uri.TryCreate(capability.Key, UriKind.Absolute, out capIdentifier) &&
                                Uri.TryCreate(capability.Value, UriKind.Absolute, out capUri))
                            {
                                newCaps[capIdentifier] = capUri;
                            }
                        }

                        // Update the capabilities for this service
                        service.Capabilities = newCaps;
                    }
                    catch (Exception ex)
                    {
                        Logger.Error("[OpenIDLogin] Failed to exchange request token for capabilities at " + service.OAuthGetAccessToken + ": " + ex.Message);
                        SendLoginTemplate(response, null, "OAuth request to " + service.OAuthGetAccessToken + " failed: " + ex.Message);
                        return;
                    }
                }
                else
                {
                    Logger.Warn("[OpenIDLogin] OAuth callback fired but there are no unfulfilled services. Could be a browser refresh");
                }

                // Check if we need to continue the cap requesting process
                GetCapabilitiesOrCompleteLogin(avatar, requestToken, response);
            }
            else
            {
                // A number of different things could lead here (incomplete login sequence, browser refresh of a completed sequence).
                // Safest thing to do would be to redirect back to the login screen
                response.Status = HttpStatusCode.Found;
                response.AddHeader("Location", new Uri(server.HttpUri, "/login/").ToString());
            }
        }

        #endregion OpenID/OAuth Handlers

        #region Linden Login Handler

        private void LindenLoginMethod(IHttpClientContext client, IHttpRequest request, IHttpResponse response)
        {
            XmlRpcRequest rpcRequest = null;
            XmlRpcResponse rpcResponse = null;

            try
            { rpcRequest = deserializer.Deserialize(new StreamReader(request.Body)) as XmlRpcRequest; }
            catch (SystemException ex)
            { Logger.Warn("Failed to deserialize incoming XML-RPC request: " + ex.Message); }

            if (rpcRequest != null)
            {
                response.ContentType = "text/xml";
                response.Encoding = Encoding.UTF8;
                response.Chunked = false;

                rpcRequest.Params.Add(request.RemoteEndPoint);
                rpcRequest.Params.Add(request.Uri);

                try
                {
                    rpcResponse = LindenLoginMethodXmlRpc(rpcRequest, request);

                    string responseString = XmlRpcResponseSerializer.Singleton.Serialize(rpcResponse);
                    byte[] buffer = Encoding.UTF8.GetBytes(responseString);
                    // Set the content-length, otherwise the LL viewer freaks out
                    response.ContentLength = buffer.Length;
                    response.Body.Write(buffer, 0, buffer.Length);
                    response.Body.Flush();
                }
                catch (Exception ex)
                {
                    Logger.ErrorFormat("XML-RPC method [{0}] threw exception: {1}", rpcRequest.MethodName, ex);

                    rpcResponse = new XmlRpcResponse();
                    rpcResponse.SetFault(INTERNAL_ERROR, String.Format("Requested method [{0}] threw exception: {1}", rpcRequest.MethodName, ex.Message));
                    XmlRpcResponseSerializer.Singleton.Serialize(new XmlTextWriter(response.Body, Encoding.UTF8), rpcResponse);
                }
            }
            else
            {
                Logger.Warn("Bad XML-RPC request");
                response.Status = HttpStatusCode.BadRequest;
            }
        }

        XmlRpcResponse LindenLoginMethodXmlRpc(XmlRpcRequest request, IHttpRequest httpRequest)
        {
            UUID sessionID;
            Avatar avatar;

            // Parse the SessionID out of the request try to retrieve an Avatar from the SessionProvider
            if (httpRequest.UriParts.Length > 0 &&
                UUID.TryParse(httpRequest.UriParts[httpRequest.UriParts.Length - 1], out sessionID) &&
                server.SessionProvider.TryGetAvatar(sessionID, out avatar))
            {
                int godLevel = avatar.GetAttribute(AvatarAttributes.GOD_LEVEL).AsInteger();

                if (godLevel >= minimumLoginLevel)
                {
                    Logger.Info("[OpenIDLogin] Accepted login for " + avatar.Identity);

                    // Update last login timestamp
                    avatar.Attributes[AvatarAttributes.LAST_LOGIN_DATE] = OSD.FromDate(DateTime.Now);
                    server.AttributeProvider.UpdateAttributes(avatar.Identity, avatar.Attributes);

                    // Return the XML-RPC login response
                    return LindenLoginHelper.LoginMethod(server, request, httpRequest, avatar, sessionID, defaultWelcomeMessage);
                }
                else
                {
                    return LindenLoginHelper.CreateLoginBlockedResponse();
                }
            }
            else
            {
                Logger.Warn("[OpenIDLogin] Login failed, missing or invalid sessionID");
            }
            
            return LindenLoginHelper.CreateLoginFailedResponse();
        }

        #endregion Linden Login Handler

        void GetCapabilitiesOrCompleteLogin(Avatar avatar, string previousRequestToken, IHttpResponse response)
        {
            OutgoingWebResponse oauthResponse;
            string requestToken;
            bool failed;
            if (GetCapabilitiesOAuth(avatar.Identity, avatar.Services, out failed, out oauthResponse, out requestToken))
            {
                // Still acquiring capabilities, repeat the OAuth process
                lock (currentServiceRequests)
                {
                    if (!String.IsNullOrEmpty(previousRequestToken))
                        currentServiceRequests.Remove(previousRequestToken);
                    currentServiceRequests[requestToken] = avatar;
                }

                // OAuth sequence starting, redirect the client
                OpenAuthHelper.OpenAuthResponseToHttp(response, oauthResponse);
            }
            else if (!failed)
            {
                // No capabilities need to be acquired, this is a successful login
                if (!String.IsNullOrEmpty(previousRequestToken))
                {
                    lock (currentServiceRequests)
                        currentServiceRequests.Remove(previousRequestToken);
                }

                // Check if this avatar is already logged in. If so, destroy the old session
                UUID sessionID;
                if (server.SessionProvider.TryGetSession(avatar.Identity, out sessionID))
                    LogoutAvatar(avatar, sessionID);

                // Create a new session
                sessionID = server.SessionProvider.CreateSession(avatar);
                Logger.Info("[OpenIDLogin] Session created for " + avatar.Identity);

                // Return the successful login template
                SendLoginSuccessTemplate(response, avatar.Identity, avatar.Attributes, sessionID);
            }
            else
            {
                SendLoginTemplate(response, null, "A required world service could not be found");
            }
        }

        bool GetCapabilitiesOAuth(Uri identity, ServiceCollection services, out bool failed, out OutgoingWebResponse webResponse, out string requestToken)
        {
            Uri[] unassociatedCaps;
            Service service = GetCurrentService(services, out unassociatedCaps);

            failed = false;
            webResponse = null;
            requestToken = null;

            if (service != null)
            {
                // Make sure we have locations for all of the OAuth endpoints for this service
                if (service.OAuthGetAccessToken != null &&
                    service.OAuthRequestToken != null &&
                    service.OAuthAuthorizeToken != null)
                {
                    // Build the list of capability methods to request
                    string[] capNames = new string[unassociatedCaps.Length];
                    for (int i = 0; i < unassociatedCaps.Length; i++)
                        capNames[i] = unassociatedCaps[i].ToString();

                    Logger.InfoFormat("[OpenIDLogin] Requesting {0} capabilities from {1} for identity {2}", capNames.Length,
                        service.OAuthGetAccessToken, identity);

                    try
                    {
                        OAuthConsumer consumer = new OAuthConsumer(OpenAuthHelper.CreateServiceProviderDescription(service), tokenManager);

                        Dictionary<string, string> extraData = new Dictionary<string, string>();
                        extraData["cb_identity"] = identity.ToString();
                        extraData["cb_capabilities"] = String.Join(",", capNames);

                        UserAuthorizationRequest authorizationRequest = consumer.PrepareRequestUserAuthorization(
                            new Uri(server.HttpUri, "/login/oauth_callback"), null, extraData);
                        webResponse = consumer.Channel.PrepareResponse(authorizationRequest);

                        if (webResponse != null)
                        {
                            requestToken = authorizationRequest.RequestToken;

                            // Return true to indicate there is another OAuth redirect
                            return true;
                        }
                        else
                        {
                            // Uh oh, something unexpected happened building the OAuth redirect
                            Logger.Error("[OpenIDLogin] Failed to initiate OAuth sequence at " + service.OAuthGetAccessToken + ", could not prepare a redirect");

                            failed = true;
                            return false;
                        }
                    }
                    catch (Exception ex)
                    {
                        Logger.Warn("[OpenIDLogin] Failed to initiate OAuth sequence at " + service.OAuthGetAccessToken + ": " + ex.Message);
                    }
                }
                else
                {
                    // Did someone forget to fill out their service definitions file?
                    Logger.Error("[OpenIDLogin] Cannot fetch capabilities for service " + service.Identifier + " through OAuth, missing one or more OAuth endpoints");

                    failed = true;
                    return false;
                }
            }

            Logger.Info("[OpenIDLogin] Remaining capabilities have been fetched through OAuth");
            return false;
        }

        Service GetCurrentService(ServiceCollection services, out Uri[] unassociatedCaps)
        {
            unassociatedCaps = null;

            foreach (KeyValuePair<Uri, Service> serviceEntry in services)
            {
                unassociatedCaps = serviceEntry.Value.GetUnassociatedCapabilities();
                if (unassociatedCaps.Length > 0)
                    return serviceEntry.Value;
            }

            return null;
        }

        void LogoutAvatar(Avatar avatar, UUID authToken)
        {
            server.SessionProvider.RemoveSession(authToken);
        }

        bool IsIdentityAuthorized(Uri identity)
        {
            string identityStr = identity.ToString();

            if (whitelist != null)
            {
                for (int i = 0; i < whitelist.Length; i++)
                {
                    if (identityStr.StartsWith(whitelist[i]))
                    {
                        Logger.Debug("Identity " + identityStr + " matched whitelist entry " + whitelist[i]);
                        return true;
                    }
                }

                Logger.Warn("Identity " + identityStr + " did not match any whitelist entries");
                return false;
            }
            else if (blacklist != null)
            {
                for (int i = 0; i < blacklist.Length; i++)
                {
                    if (identityStr.StartsWith(blacklist[i]))
                    {
                        Logger.Warn("Identity " + identityStr + " blacklisted by entry " + blacklist[i]);
                        return false;
                    }
                }
            }

            return true;
        }

        void SendLoginTemplate(IHttpResponse response, string infoMessage, string errorMessage)
        {
            IDictionary<string, object> variables = new Dictionary<string, object>();
            variables["has_error"] = !String.IsNullOrEmpty(errorMessage);
            variables["has_message"] = !String.IsNullOrEmpty(infoMessage);
            variables["error"] = errorMessage != null ? errorMessage : String.Empty;
            variables["message"] = infoMessage != null ? infoMessage : String.Empty;

            try
            {
                string output = server.HttpTemplates.Render(LOGIN_PAGE, variables);
                byte[] data = Encoding.UTF8.GetBytes(output);
                response.ContentLength = data.Length;
                response.ContentType = "text/html";
                response.Body.Write(data, 0, data.Length);
            }
            catch (Exception ex)
            {
                Logger.Error("[OpenIDLogin] Failed to render " + LOGIN_PAGE + " template: " + ex.Message);
            }
        }

        protected virtual void SendLoginSuccessTemplate(IHttpResponse response, Uri identity, Dictionary<Uri, OSD> attributes, UUID sessionID)
        {
            string loginUri = server.HttpUri + "login/" + sessionID;

            Dictionary<string, object> variables = new Dictionary<string, object>();
            variables["identity"] = identity;
            variables["login_uri"] = loginUri;
            variables["cb_login_uri"] = "cablebeach://" + HttpUtility.UrlEncode(server.HttpUri + "login/" + sessionID);

            try
            {
                string output = server.HttpTemplates.Render(LOGIN_SUCCESS_PAGE, variables);
                byte[] data = Encoding.UTF8.GetBytes(output);
                response.ContentLength = data.Length;
                response.ContentType = "text/html";
                response.Cookies.Add(new ResponseCookie("sessionID", sessionID.ToString(), DateTime.Now + TimeSpan.FromDays(1)));
                response.Body.Write(data, 0, data.Length);
            }
            catch (Exception ex)
            {
                Logger.Error("[OpenIDLogin] Failed to render " + LOGIN_SUCCESS_PAGE + " template: " + ex.Message);
            }
        }

        static OSD GetAttributeValue(Dictionary<Uri, OSD> attributes, Uri identifier)
        {
            OSD value;
            if (attributes.TryGetValue(identifier, out value))
                return value;
            else
                return new OSD();
        }

        static Uri RemoveQuery(Uri uri)
        {
            return new Uri(uri.Scheme + "://" + uri.Authority + uri.AbsolutePath);
        }
    }
}
